Go 的 log 库
Go 标准库提供了一个 日志库 log
快速使用
log 默认输出到标准错误(stderr),每条日志前会自动加上日期和时间。如果日志不是以换行符结尾的,那么 log 会自动加上换行符。即每条日志会在新行中输出。
log 提供了三组函数:
- Print/Printf/Println:正常输出日志;
- Panic/Panicf/Panicln:输出日志后,以拼装好的字符串为参数调用panic;
- Fatal/Fatalf/Fatalln:输出日志后,调用os.Exit(1)退出程序。
使用例:
package main
import (
"log"
)
type User struct {
Name string
Age int
}
func main() {
u := User{
Name: "dj",
Age: 18,
}
log.Printf("%s login, age:%d", u.Name, u.Age)
log.Fatalf("Danger! hacker %s login", u.Name) // 直接异常结束
log.Panicf("Oh, system error when %s login", u.Name) // 抛出 panic 异常
}
打印效果:
2021/10/26 21:07:08 dj login, age:18
2021/10/26 21:07:08 Danger! hacker dj login
exit status 1
因为 Fatalf 已经结束了所以看不到 Panicf 的效果,下面打印 Panicf 的返回效果
panic: Oh, system error when dj login
goroutine 1 [running]:
log.Panicf({0x10933c0, 0x2}, {0xc00007bf40, 0xc00007bf70, 0xfe52b9})
C:/Program Files/Go/src/log/log.go:361 +0x67
main.main()
C:/Users/33204/Desktop/Go/strpc/main.go:19 +0xcb
exit status 2
定制打印格式
添加前缀
调用 log.SetPrefix
为每条日志文本前增加一个前缀。例如,在上面的程序中设置Login:前缀:
func main() {
u := User{
Name: "dj",
Age: 18,
}
log.SetPrefix("Login: ")
log.Printf("%s login, age:%d", u.Name, u.Age)
}
调用 log.Prefix
可以获取当前设置的前缀。
打印效果:
Login: 2021/10/26 21:12:01 dj login, age:18
添加额外信息 Flag
设置选项可在每条输出的文本前增加一些额外信息,如日期时间、文件名等。
// src/log/log.go
const (
Ldate = 1 << iota // 输出当地时区的日期,如 2021/10/26;
Ltime // 输出当地时区的时间,如 21:12:01;
Lmicroseconds // 输出的时间精确到微秒,设置了该选项就不用设置Ltime了。如11:45:45.123123;
Llongfile // 输出长文件名+行号,含包名,如github.com/darjun/go-daily-lib/log/flag/main.go:50;
Lshortfile // 输出短文件名+行号,不含包名,如main.go:50;
LUTC // 如果设置了Ldate或Ltime,将输出 UTC 时间,而非当地时区。
)
调用 log.SetFlag
设置选项,可以一次设置多个:
func main() {
u := User{
Name: "dj",
Age: 18,
}
// 默认是 Ldate | Ltime
log.SetFlags(log.Lshortfile | log.Ldate | log.Lmicroseconds)
log.Printf("%s login, age:%d", u.Name, u.Age)
}
打印结果
2021/10/26 21:14:53.221404 main.go:19: dj login, age:18
自定义 Log
标准 Logger 实现
log 库为我们定义了一个默认的 Logger,名为 std,意为标准日志。直接调用的 log 库的方法,其内部是调用 std 的对应方法:
// src/log/log.go
var std = New(os.Stderr, "", LstdFlags)
func Printf(format string, v ...interface{}) {
std.Output(2, fmt.Sprintf(format, v...))
}
func Fatalf(format string, v ...interface{}) {
std.Output(2, fmt.Sprintf(format, v...))
os.Exit(1)
}
func Panicf(format string, v ...interface{}) {
s := fmt.Sprintf(format, v...)
std.Output(2, s)
panic(s)
}
自定义 Logger
如下定义自己的 Logger
package main
import (
"bytes"
"fmt"
"log"
)
type User struct {
Name string
Age int
}
func main() {
u := User{
Name: "dj",
Age: 18,
}
buf := &bytes.Buffer{}
logger := log.New(buf, "", log.Lshortfile|log.LstdFlags)
logger.Printf("%s login, age:%d", u.Name, u.Age)
fmt.Print(buf.String())
}
log.New 接受三个参数
io.Writer
:日志都会写到这个 Writer 中;prefix
:前缀,也可以后面调用logger.SetPrefix
设置;flag
:选项,也可以后面调用logger.SetFlag
设置。
上面代码将日志输出到一个 bytes.Buffer
,然后将这个 buf 打印到标准输出。可以注意到,第一个参数为 io.Writer
,所以可以使用 io.MultiWriter
实现多目的地输出。下面我们将日志同时输出到 标准输出、 bytes.Buffer
和文件中:
package main
import (
"bytes"
"io"
"log"
"os"
)
type User struct {
Name string
Age int
}
func main() {
u := User{
Name: "dj",
Age: 18,
}
writer1 := &bytes.Buffer{}
writer2 := os.Stdout
writer3, err := os.OpenFile("log.txt", os.O_WRONLY|os.O_CREATE, 0755)
if err != nil {
log.Fatalf("create file log.txt failed: %v", err)
}
logger := log.New(io.MultiWriter(writer1, writer2, writer3), "", log.Lshortfile|log.LstdFlags)
logger.Printf("%s login, age:%d", u.Name, u.Age)
}
打印的颜色
如何设置打印的颜色呢?
package log
import (
"log"
"os"
)
var (
errorLog = log.New(os.Stdout, "\033[31m[error]\033[0m ", log.LstdFlags|log.Lshortfile)
infoLog = log.New(os.Stdout, "\033[34m[info ]\033[0m ", log.LstdFlags|log.Lshortfile)
)
// log methods
var (
Error = errorLog.Println
Errorf = errorLog.Printf
Info = infoLog.Println
Infof = infoLog.Printf
)
[info ]
颜色为蓝色,[error]
为红色。使用 log.Lshortfile
支持显示文件名和代码行号。
丢弃输出结果
可以通过 ioutil.Discard
来将打印结果丢弃
errorLog.SetOutput(ioutil.Discard)
编写文件日志模块
简单封装 log 库,使其支持简单的文件日志
新建 logging 目录,新建 file.go
和 log.go
文件,写入内容:
设置日志文件
file.go:
package logging
import (
"fmt"
"log"
"os"
"time"
)
var (
LogSavePath = "runtime/logs/"
LogSaveName = "log"
LogFileExt = "log"
TimeFormat = "20060102"
)
func getLogFilePath() string {
return fmt.Sprintf("%s", LogSavePath)
}
func getLogFileFullPath() string {
prefixPath := getLogFilePath()
suffixPath := fmt.Sprintf("%s%s.%s", LogSaveName, time.Now().Format(TimeFormat), LogFileExt)
return fmt.Sprintf("%s%s", prefixPath, suffixPath)
}
func openLogFile(filePath string) *os.File {
_, err := os.Stat(filePath)
switch {
case os.IsNotExist(err):
mkDir()
case os.IsPermission(err):
log.Fatalf("Permission :%v", err)
}
handle, err := os.OpenFile(filePath, os.O_APPEND | os.O_CREATE | os.O_WRONLY, 0644)
if err != nil {
log.Fatalf("Fail to OpenFile :%v", err)
}
return handle
}
func mkDir() {
// Getwd 返回与当前目录对应的根路径名
dir, _ := os.Getwd()
// MkdirAll 创建对应的目录以及所需的子目录,若成功则返回 nil,否则返回 error
err := os.MkdirAll(dir + "/" + getLogFilePath(), os.ModePerm)
if err != nil {
panic(err)
}
}
编写 Logger
log.go 文件
package logging
import (
"fmt"
"log"
"os"
"path/filepath"
"runtime"
)
type Level int
var (
F *os.File
DefaultPrefix = ""
DefaultCallerDepth = 2
logger *log.Logger
logPrefix = ""
levelFlags = []string{"DEBUG", "INFO", "WARN", "ERROR", "FATAL"}
)
const (
DEBUG Level = iota
INFO
WARNING
ERROR
FATAL
)
func init() {
filePath := getLogFileFullPath()
F = openLogFile(filePath)
logger = log.New(F, DefaultPrefix, log.LstdFlags)
}
func Debug(v ...interface{}) {
setPrefix(DEBUG)
logger.Println(v)
}
func Info(v ...interface{}) {
setPrefix(INFO)
logger.Println(v)
}
func Warn(v ...interface{}) {
setPrefix(WARNING)
logger.Println(v)
}
func Error(v ...interface{}) {
setPrefix(ERROR)
logger.Println(v)
}
func Fatal(v ...interface{}) {
setPrefix(FATAL)
logger.Fatalln(v)
}
func setPrefix(level Level) {
_, file, line, ok := runtime.Caller(DefaultCallerDepth)
if ok {
logPrefix = fmt.Sprintf("[%s][%s:%d]", levelFlags[level], filepath.Base(file), line)
} else {
logPrefix = fmt.Sprintf("[%s]", levelFlags[level])
}
logger.SetPrefix(logPrefix)
}
运行后,测试打印
logging.Info(err.Key, err.Message)
检查输出的文件:
[INFO][auth.go:45]2021/11/03 21:38:33 [Username.Required. Username Can not be empty]
[INFO][auth.go:45]2021/11/03 21:38:33 [Password.Required. Password Can not be empty]
[INFO][auth.go:45]2021/11/03 21:39:36 [Password.Required. Password Can not be empty]
[INFO][auth.go:45]2021/11/03 21:40:45 [Password.Required. Password Can not be empty]
[INFO][auth.go:45]2021/11/03 21:40:56 [Password.Required. Password Can not be empty]
错误日志
对于错误日志,它有 Fatal 和 Panic
- Fatal 日志通过调用
os.Exit(1)
来结束程序 - Panic 日志在写入日志消息之后抛出一个 panic
但是它缺少一个 ERROR 日志级别,这个级别可以在不抛出 panic 或退出程序的情况下记录错误